Laravel Database——Eloquent Model 关联源码分析

前言

数据库表通常相互关联。laravel 中的模型关联功能使得关于数据库的关联代码变得更加简单,更加优雅。本文会详细说说关于模型关联的源码,以便更好的理解和使用关联模型。官方文档:Eloquent:关联

定义关联

所谓的定义关联,就是在一个 Model 中定义一个关联函数,我们利用这个关联函数去操作另外一个 Model,例如,user 表是用户表,posts 是用户发的文章,一个用户可以发表多篇文章,我们就可以这样写:

  1. $user->posts()->where('active', 1)->get();

这表明了我们想通过 $user 这个用户查询到状态 active 为 1 的所有文章,posts 就是关联函数,我们可以通过这个关联函数去操作另一个与 user 关联的表。

在说模型关联的定义之前,我们要先说说父模型与子模型的概念。所谓的父模型是指在模型关系中主动的一方,例如用户模型和文章模型中的用户,相应的子模型就是模型关系中的被动一方,例如文章模型。在正向定义中,被关联的是子模型,而在反向关联中,被关联的是父模型。

我们知道,关联有多种形式,各种关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图1

hasOne 一对一

我们以官方文档的例子来说明,一个 User 模型可能关联一个 Phone 模型:

  1. class User extends Model
  2. {
  3. /**
  4. * 获得与用户关联的电话记录。
  5. */
  6. public function phone()
  7. {
  8. $this->hasOne('App\Phone', 'user_id', 'id');
  9. }
  10. }

我们来看看 hasOne 的源码:

  1. public function hasOne($related, $foreignKey = null, $localKey = null)
  2. {
  3. $instance = $this->newRelatedInstance($related);
  4. $foreignKey = $foreignKey ?: $this->getForeignKey();
  5. $localKey = $localKey ?: $this->getKeyName();
  6. return new HasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
  7. }

newRelatedInstance 函数负责建立一个新的被关联的模型实例,主要目的是设置数据库连接:

  1. protected function newRelatedInstance($class)
  2. {
  3. return tap(new $class, function ($instance) {
  4. if (! $instance->getConnectionName()) {
  5. $instance->setConnection($this->connection);
  6. }
  7. });
  8. }

在一对一的关系中,foreignKey 外键名默认是父模型的类名和主键名的蛇形变量,localKey 是父模型的主键名:

  1. public function getForeignKey()
  2. {
  3. return Str::snake(class_basename($this)).'_'.$this->primaryKey;
  4. }

hasOne 函数的构造函数继承 HasOneOrMany 类,也就是说,一对一与一对多构造函数相同,这部分主要设置外键名:

  1. public function __construct(Builder $query, Model $parent, $foreignKey, $localKey)
  2. {
  3. $this->localKey = $localKey;
  4. $this->foreignKey = $foreignKey;
  5. parent::__construct($query, $parent);
  6. }

HasOneOrMany 类继承 Relation 类,这部分主要设置 parent (父模型)、被关联模型(子模型)与被关联模型(子模型)的查询构造器:

  1. public function __construct(Builder $query, Model $parent)
  2. {
  3. $this->query = $query;
  4. $this->parent = $parent;
  5. $this->related = $query->getModel();
  6. $this->addConstraints();
  7. }

hasOne 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图2

除了保存被关联模型的查询构造器、被关联模型与 parent 模型之外,还会提供额外的限制条件:

  1. public function addConstraints()
  2. {
  3. if (static::$constraints) {
  4. $this->query->where($this->foreignKey, '=', $this->getParentKey());
  5. $this->query->whereNotNull($this->foreignKey);
  6. }
  7. }
  8. public function getParentKey()
  9. {
  10. return $this->parent->getAttribute($this->localKey);
  11. }

限制条件为被关联模型和关联模型建立外键约束关系:

  1. select phone where phone.user_id = 1 (user.id)

hasMany 一对多

在模型关联的定义中,一对一与一对多源码是一样的:

  1. public function hasMany($related, $foreignKey = null, $localKey = null)
  2. {
  3. $instance = $this->newRelatedInstance($related);
  4. $foreignKey = $foreignKey ?: $this->getForeignKey();
  5. $localKey = $localKey ?: $this->getKeyName();
  6. return new HasMany(
  7. $instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
  8. );
  9. }

hasMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图3

限制条件与一对一相同,为被关联模型和关联模型建立外键约束关系:

  1. select phone where phone.user_id = 1 (user.id)

belongsTo 一对一、一对多 反向关联

如果想要从文章反向查找作者用户,那么可以定义反向关联:

  1. public function user()
  2. {
  3. return $this->belongsTo('App\User', 'foreign_key', 'other_key');
  4. }

belongsTo 源码:

  1. public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
  2. {
  3. if (is_null($relation)) {
  4. $relation = $this->guessBelongsToRelation();
  5. }
  6. $instance = $this->newRelatedInstance($related);
  7. if (is_null($foreignKey)) {
  8. $foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
  9. }
  10. $ownerKey = $ownerKey ?: $instance->getKeyName();
  11. return new BelongsTo(
  12. $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
  13. );
  14. }

正向定义与反向定义不同的是多了一个参数 relation,这个参数默认值是从 debug_backtrace 函数获取的:

  1. protected function guessBelongsToRelation()
  2. {
  3. list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
  4. return $caller['function'];
  5. }

也就是我们的关联函数名 userbelongsTo 函数会将关联函数名作为关联名保存起来。

另一个不同是外键的默认名称,不再是类名 + 主键名,而是关联名 + 主键名:

  1. if (is_null($foreignKey)) {
  2. $foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
  3. }

我们接着看 belongsTo 函数:

  1. public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relation)
  2. {
  3. $this->ownerKey = $ownerKey;
  4. $this->relation = $relation;
  5. $this->foreignKey = $foreignKey;
  6. $this->child = $child;
  7. parent::__construct($query, $child);
  8. }

我们可以看出来,相对于正向关联,反向关联除了保存外键名与主键名之外,还保存了关系名、子模型。值得注意的是,反向关联中 related 代表父模型,parent 代表子模型,与正向关联相反。

hasMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图4

约束条件也相应地进行反转改变:

  1. public function addConstraints()
  2. {
  3. if (static::$constraints) {
  4. $table = $this->related->getTable();
  5. $this->query->where($table.'.'.$this->ownerKey, '=', $this->child->{$this->foreignKey});
  6. }
  7. }

限制条件:

  1. select user where user.id = 1 (post.user_id)

belongsMany 多对多

多对多关系由于中间表的原因相对来说比较复杂,涉及的参数也非常多。我们以官网例子:

  1. class User extends Model
  2. {
  3. /**
  4. * 获得此用户的角色。
  5. */
  6. public function roles()
  7. {
  8. return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
  9. }
  10. }

User 表与 role 表是多对多关系,另外有一中间表 user_role 表,我们在定义关系的时候,related 是被关联模型,table 是中间表,foreignPivotKey 是中间表中父模型外键名,relatedPivotKey 是中间表中子模型外键名,parentKey 是父模型主键名,relatedKey 是子模型主键名,relation 是关系名。

  1. public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null)
  2. {
  3. if (is_null($relation)) {
  4. $relation = $this->guessBelongsToManyRelation();
  5. }
  6. $instance = $this->newRelatedInstance($related);
  7. $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
  8. $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
  9. if (is_null($table)) {
  10. $table = $this->joiningTable($related);
  11. }
  12. return new BelongsToMany(
  13. $instance->newQuery(), $this, $table, $foreignPivotKey,
  14. $relatedPivotKey, $parentKey ?: $this->getKeyName(),
  15. $relatedKey ?: $instance->getKeyName(), $relation
  16. );
  17. }

获取关联名称仍然使用的是 debug_backtrace 函数,不同于guessBelongsToRelation 函数只有 belongsTo 调用, guessBelongsToManyRelation 函数还可以被 morphedByMany 函数调用,所以不能单纯的限制返回堆栈帧:

  1. public static $manyMethods = [
  2. 'belongsToMany', 'morphToMany', 'morphedByMany',
  3. 'guessBelongsToManyRelation', 'findFirstMethodThatIsntRelation',
  4. ];
  5. protected function guessBelongsToManyRelation()
  6. {
  7. $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($trace) {
  8. return ! in_array($trace['function'], Model::$manyMethods);
  9. });
  10. return ! is_null($caller) ? $caller['function'] : null;
  11. }

默认的中间表是两个表名的蛇形变量:

  1. public function joiningTable($related)
  2. {
  3. $models = [
  4. Str::snake(class_basename($related)),
  5. Str::snake(class_basename($this)),
  6. ];
  7. sort($models);
  8. return strtolower(implode('_', $models));
  9. }

BelongsToMany 的初始化也需要保存这些变量:

  1. public function __construct(Builder $query, Model $parent, $table, $foreignPivotKey,
  2. $relatedPivotKey, $parentKey, $relatedKey, $relationName = null)
  3. {
  4. $this->table = $table;
  5. $this->parentKey = $parentKey;
  6. $this->relatedKey = $relatedKey;
  7. $this->relationName = $relationName;
  8. $this->relatedPivotKey = $relatedPivotKey;
  9. $this->foreignPivotKey = $foreignPivotKey;
  10. parent::__construct($query, $parent);
  11. }

belongsToMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图5

反向的多对多模型关系:

Laravel Database——Eloquent Model 关联源码分析 - 图6

限制条件:

  1. public function addConstraints()
  2. {
  3. $this->performJoin();
  4. if (static::$constraints) {
  5. $this->addWhereConstraints();
  6. }
  7. }
  8. protected function performJoin($query = null)
  9. {
  10. $query = $query ?: $this->query;
  11. $baseTable = $this->related->getTable();
  12. $key = $baseTable.'.'.$this->relatedKey;
  13. $query->join($this->table, $key, '=', $this->getQualifiedRelatedPivotKeyName());
  14. return $this;
  15. }
  16. protected function addWhereConstraints()
  17. {
  18. $this->query->where(
  19. $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
  20. );
  21. return $this;
  22. }

本例中的 where 条件:

  1. select role join role_user on role_user.role_id = 1 (role.id)
  2. select role where role_user.user_id = 1 (user.id)

hasManyThrough 远程一对多

远层一对多 关联提供了方便、简短的方式通过中间的关联来获得远层的关联。以官方例子来看:

  1. class Country extends Model
  2. {
  3. public function posts()
  4. {
  5. return $this->hasManyThrough(
  6. 'App\Post',
  7. 'App\User',
  8. 'country_id', // 用户表外键...
  9. 'user_id', // 文章表外键...
  10. 'id', // 国家表本地键...
  11. 'id' // 用户表本地键...
  12. );
  13. }
  14. }

可以看到,远程一对多的参数比较多。第一个参数 related 是最终被关联的模型,through 是中间模型,firstKey 是中间模型关于父模型的外键,secondKey 是最终被关联的模型关于中间模型的外键,localKey 是父模型的主键,secondLocalKey 是中间模型的主键:

  1. public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
  2. {
  3. $through = new $through;
  4. $firstKey = $firstKey ?: $this->getForeignKey();
  5. $secondKey = $secondKey ?: $through->getForeignKey();
  6. $localKey = $localKey ?: $this->getKeyName();
  7. $secondLocalKey = $secondLocalKey ?: $through->getKeyName();
  8. $instance = $this->newRelatedInstance($related);
  9. return new HasManyThrough($instance->newQuery(), $this, $through, $firstKey, $secondKey, $localKey, $secondLocalKey);
  10. }

HasManyThrough 的初始化:

  1. public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
  2. {
  3. $this->localKey = $localKey;
  4. $this->firstKey = $firstKey;
  5. $this->secondKey = $secondKey;
  6. $this->farParent = $farParent;
  7. $this->throughParent = $throughParent;
  8. $this->secondLocalKey = $secondLocalKey;
  9. parent::__construct($query, $throughParent);
  10. }

hasManyThrough 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图7

限制条件:

  1. public function addConstraints()
  2. {
  3. $localValue = $this->farParent[$this->localKey];
  4. $this->performJoin();
  5. if (static::$constraints) {
  6. $this->query->where($this->getQualifiedFirstKeyName(), '=', $localValue);
  7. }
  8. }
  9. protected function performJoin(Builder $query = null)
  10. {
  11. $query = $query ?: $this->query;
  12. $farKey = $this->getQualifiedFarKeyName();
  13. $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
  14. if ($this->throughParentSoftDeletes()) {
  15. $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
  16. }
  17. }
  18. public function getQualifiedParentKeyName()
  19. {
  20. return $this->parent->getTable().'.'.$this->secondLocalKey;
  21. }
  22. public function getQualifiedFarKeyName()
  23. {
  24. return $this->getQualifiedForeignKeyName();
  25. }
  26. public function getQualifiedForeignKeyName()
  27. {
  28. return $this->related->getTable().'.'.$this->secondKey;
  29. }
  30. public function getQualifiedFirstKeyName()
  31. {
  32. return $this->throughParent->getTable().'.'.$this->firstKey;
  33. }

本例中的限制条件:

  1. select post join user on user.id = post.user_id
  2. select post where user.delete_at is null
  3. select post where user.country_id = 1 (country.id)

morphOne/morphMany 多态关联

多态关联允许我们应用一个表来单独作为多个表的属性,多态关联存在一对一、一对多、多对多的情形。所谓一对一、一对多是指,一个模型只拥有一个属性或多个属性,例如官网中的例子:

用户可以「评论」文章和视频。使用多态关联,您可以用一个 comments 表同时满足这两个使用场景

  1. class Post extends Model
  2. {
  3. /**
  4. * 获得此文章的所有评论。
  5. */
  6. public function comments()
  7. {
  8. return $this->morphMany('App\Comment', 'commentable');
  9. }
  10. }
  11. class Video extends Model
  12. {
  13. /**
  14. * 获得此视频的所有评论。
  15. */
  16. public function comments()
  17. {
  18. return $this->morphMany('App\Comment', 'commentable');
  19. }
  20. }

这个 comments 表就是属性表,当文章和视频只能有一个评论的时候,那么就是一对一多态关联;如果文章和视频可以由多个评论的时候,就是一对多多态关联。

这种属性表一般会有两个固定的字段:commentable_type 用于标识该条评论是文章的还是视频的、commentable_id 用于记录文章或视频的主键 id

我们可以把多态关联看作普通的一对一、一对多关系,只是外键参数是 typeid 的组合。

related 是属性表,也就是这里的 commentstype 参数是属性表中存储父模型类型的列名(commentable_type),id 参数是属性表中存储父模型主键的列名(commentable_id),而 name 专用于省略 type 参数与 id 参数,localKey 是指父模型的主键。

  1. public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
  2. {
  3. $instance = $this->newRelatedInstance($related);
  4. list($type, $id) = $this->getMorphs($name, $type, $id);
  5. $table = $instance->getTable();
  6. $localKey = $localKey ?: $this->getKeyName();
  7. return new MorphOne($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
  8. }
  9. public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
  10. {
  11. $instance = $this->newRelatedInstance($related);
  12. list($type, $id) = $this->getMorphs($name, $type, $id);
  13. $table = $instance->getTable();
  14. $localKey = $localKey ?: $this->getKeyName();
  15. return new MorphMany($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey);
  16. }
  17. protected function getMorphs($name, $type, $id)
  18. {
  19. return [$type ?: $name.'_type', $id ?: $name.'_id'];
  20. }

一对一、一对多多态关联主要保存属性表中表示类型的列名,还有需要向该类型列中写入的父模型名称,一般来说,默认会写父模型的类名(App\PostApp\Video)

  1. public function __construct(Builder $query, Model $parent, $type, $id, $localKey)
  2. {
  3. $this->morphType = $type;
  4. $this->morphClass = $parent->getMorphClass();
  5. parent::__construct($query, $parent, $id, $localKey);
  6. }
  7. public function getMorphClass()
  8. {
  9. $morphMap = Relation::morphMap();
  10. if (! empty($morphMap) && in_array(static::class, $morphMap)) {
  11. return array_search(static::class, $morphMap, true);
  12. }
  13. return static::class;
  14. }

不过我们也可以自定义写入的值:

  1. Relation::morphMap([
  2. 'posts' => 'App\Post',
  3. 'videos' => 'App\Video',
  4. ]);

这样,就会把 App\Post 换成 postsApp\Video 换成 videos。我们来看看这个 多态映射表 函数:

  1. public static function morphMap(array $map = null, $merge = true)
  2. {
  3. $map = static::buildMorphMapFromModels($map);
  4. if (is_array($map)) {
  5. static::$morphMap = $merge && static::$morphMap
  6. ? array_merge(static::$morphMap, $map) : $map;
  7. }
  8. return static::$morphMap;
  9. }
  10. protected static function buildMorphMapFromModels(array $models = null)
  11. {
  12. if (is_null($models) || Arr::isAssoc($models)) {
  13. return $models;
  14. }
  15. return array_combine(array_map(function ($model) {
  16. return (new $model)->getTable();
  17. }, $models), $models);
  18. }

可以看到,buildMorphMapFromModels 函数将字符串 App\Post 转为 model,并利用 array_combine 转为键。

morphOne 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图8

morphMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图9

限制条件:

  1. public function addConstraints()
  2. {
  3. if (static::$constraints) {
  4. parent::addConstraints();
  5. $this->query->where($this->morphType, $this->morphClass);
  6. }
  7. }
  8. public function addConstraints()
  9. {
  10. if (static::$constraints) {
  11. $this->query->where($this->foreignKey, '=', $this->getParentKey());
  12. $this->query->whereNotNull($this->foreignKey);
  13. }
  14. }

本例中的限制条件:

  1. select comments where comment.commentable_id = post.id
  2. select comments where comment.commentable_id is not null
  3. select comments where comment.commentable_type = 'App\Post'

morphTo 反向多态关联

和一对一、一对多的 belongsTo 相似,多态关联还可以定义反向关联 morphTo:

  1. class Comment extends Model
  2. {
  3. /**
  4. * 获得拥有此评论的模型。
  5. */
  6. public function commentable()
  7. {
  8. return $this->morphTo();
  9. }
  10. }

belongsTo 类似,morphTo 也是利用 debug_backtrace 获取关联名称。当前如果正处于预加载状态的时候,Comment 一般还没有从数据库获取数据,$this->{$type} 是空值,这个时候需要去除预加载来初始化:

  1. public function morphTo($name = null, $type = null, $id = null)
  2. {
  3. $name = $name ?: $this->guessBelongsToRelation();
  4. list($type, $id) = $this->getMorphs(
  5. Str::snake($name), $type, $id
  6. );
  7. return empty($class = $this->{$type})
  8. ? $this->morphEagerTo($name, $type, $id)
  9. : $this->morphInstanceTo($class, $name, $type, $id);
  10. }
  11. protected function morphEagerTo($name, $type, $id)
  12. {
  13. return new MorphTo(
  14. $this->newQuery()->setEagerLoads([]), $this, $id, null, $type, $name
  15. );
  16. }
  17. protected function morphInstanceTo($target, $name, $type, $id)
  18. {
  19. $instance = $this->newRelatedInstance(
  20. static::getActualClassNameForMorph($target)
  21. );
  22. return new MorphTo(
  23. $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name
  24. );
  25. }

多态的成员变量 morphType 代表属性表的类型列,morphClass

MorphTo 的成员变量只有一个 morphType:

  1. public function __construct(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation)
  2. {
  3. $this->morphType = $type;
  4. parent::__construct($query, $parent, $foreignKey, $ownerKey, $relation);
  5. }

morphTo 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图10

限制条件与 belongsTo 相同:

  1. public function addConstraints()
  2. {
  3. if (static::$constraints) {
  4. $table = $this->related->getTable();
  5. $this->query->where($table.'.'.$this->ownerKey, '=', $this->child->{$this->foreignKey});
  6. }
  7. }

本例中的限制条件

  1. select post where post.id = comments.commentable_id

多对多多态关联

除了传统的多态关联,您也可以定义「多对多」的多态关联。例如,Post 模型和 Video 模型可以共享一个多态关联至 Tag 模型。 使用多对多多态关联可以让您在文章和视频中共享唯一的标签列表。

  1. class Post extends Model
  2. {
  3. /**
  4. * 获得此文章的所有标签。
  5. */
  6. public function tags()
  7. {
  8. return $this->morphToMany('App\Tag', 'taggable');
  9. }
  10. }

多对多多态关联与多对多关联的代码类似,不同的是中间表不再是两个父模型的蛇形变量,而是 name 的复数,值得注意的是 foreignPivotKey 代表中间表中对当前 post 或者 video 的外键,一般会放在 taggable_id 字段中,relatedPivotKey 代表中间表中对属性表 tag 的外键 tag_id:

  1. public function morphToMany($related, $name, $table = null, $foreignPivotKey = null,
  2. $relatedPivotKey = null, $parentKey = null,
  3. $relatedKey = null, $inverse = false)
  4. {
  5. $caller = $this->guessBelongsToManyRelation();
  6. $instance = $this->newRelatedInstance($related);
  7. $foreignPivotKey = $foreignPivotKey ?: $name.'_id';
  8. $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
  9. $table = $table ?: Str::plural($name);
  10. return new MorphToMany(
  11. $instance->newQuery(), $this, $name, $table,
  12. $foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(),
  13. $relatedKey ?: $instance->getKeyName(), $caller, $inverse
  14. );
  15. }

MorphToMany 的构造函数依然有 morphTypemorphClassmorphType 标识着当前中间表的记录类型是 Post,还是 videosmorphClass 的值默认值是 Post 类或者 videos 的全名,正向关联的时候,inversefalse,反向关联的时候, inversetrue

  1. public function __construct(Builder $query, Model $parent, $name, $table, $foreignPivotKey,
  2. $relatedPivotKey, $parentKey, $relatedKey, $relationName = null, $inverse = false)
  3. {
  4. $this->inverse = $inverse;
  5. $this->morphType = $name.'_type';
  6. $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass();
  7. parent::__construct(
  8. $query, $parent, $table, $foreignPivotKey,
  9. $relatedPivotKey, $parentKey, $relatedKey, $relationName
  10. );
  11. }

正向关联的时候,parent 类是 Post 类或者 videos 类,反向关联的时候 relatedPost 类或者 videos 类。

限制条件:

  1. protected function addWhereConstraints()
  2. {
  3. parent::addWhereConstraints();
  4. $this->query->where($this->table.'.'.$this->morphType, $this->morphClass);
  5. return $this;
  6. }
  7. protected function addWhereConstraints()
  8. {
  9. $this->query->where(
  10. $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
  11. );
  12. return $this;
  13. }
  14. public function getQualifiedForeignPivotKeyName()
  15. {
  16. return $this->table.'.'.$this->foreignPivotKey;
  17. }

官网中例子限制条件转化为 sql (假设 Post 的主键为 1) :

  1. where taggables.taggable_id = 1;
  2. where taggables.taggable_type = 'App\Post'

morphToMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图11

限制条件:

  1. public function addConstraints()
  2. {
  3. $this->performJoin();
  4. if (static::$constraints) {
  5. $this->addWhereConstraints();
  6. }
  7. }
  8. protected function performJoin($query = null)
  9. {
  10. $query = $query ?: $this->query;
  11. $baseTable = $this->related->getTable();
  12. $key = $baseTable.'.'.$this->relatedKey;
  13. $query->join($this->table, $key, '=', $this->getQualifiedRelatedPivotKeyName());
  14. return $this;
  15. }
  16. protected function addWhereConstraints()
  17. {
  18. parent::addWhereConstraints();
  19. $this->query->where($this->table.'.'.$this->morphType, $this->morphClass);
  20. return $this;
  21. }
  22. protected function addWhereConstraints()
  23. {
  24. $this->query->where(
  25. $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
  26. );
  27. return $this;
  28. }

本例中的限制条件:

  1. select tag join tagable on tagable.tag_id = tag.id
  2. select tags where tagable.tagables_id = post.id
  3. select tags where tagable.tagables_type = 'App\Tag'

多对多多态反向关联

官方文档例子:

  1. class Tag extends Model
  2. {
  3. /**
  4. * 获得此标签下所有的文章。
  5. */
  6. public function posts()
  7. {
  8. return $this->morphedByMany('App\Post', 'taggable');
  9. }
  10. }

与正向关联相反,relatedPivotKey 代表中间表中对 relatedpost 或者 video 的外键,一般会放在 taggable_id 字段中,foreignPivotKey 代表中间表中对当前属性表 tag 的外键 tag_id

  1. public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null,
  2. $relatedPivotKey = null, $parentKey = null, $relatedKey = null)
  3. {
  4. $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
  5. $relatedPivotKey = $relatedPivotKey ?: $name.'_id';
  6. return $this->morphToMany(
  7. $related, $name, $table, $foreignPivotKey,
  8. $relatedPivotKey, $parentKey, $relatedKey, true
  9. );
  10. }

官网中例子限制条件转化为 sql (假设 Tag 的主键为 1) :

  1. where taggables.tag_id = 1;
  2. where taggables.taggable_type = 'App\Post'

morphedByMany 的模型关系如下:

Laravel Database——Eloquent Model 关联源码分析 - 图12

限制条件与 morphToMany 一致:

  1. public function addConstraints()
  2. {
  3. $this->performJoin();
  4. if (static::$constraints) {
  5. $this->addWhereConstraints();
  6. }
  7. }
  8. protected function performJoin($query = null)
  9. {
  10. $query = $query ?: $this->query;
  11. $baseTable = $this->related->getTable();
  12. $key = $baseTable.'.'.$this->relatedKey;
  13. $query->join($this->table, $key, '=', $this->getQualifiedRelatedPivotKeyName());
  14. return $this;
  15. }
  16. protected function addWhereConstraints()
  17. {
  18. parent::addWhereConstraints();
  19. $this->query->where($this->table.'.'.$this->morphType, $this->morphClass);
  20. return $this;
  21. }
  22. protected function addWhereConstraints()
  23. {
  24. $this->query->where(
  25. $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
  26. );
  27. return $this;
  28. }

本例中的限制条件

  1. select post join post on post.id = tagables.tagable_id
  2. select post where tagables.tag_id = tag.id
  3. select post where tagables.tagable_type = 'App\Post'